Skip to content

dash_charts.gantt_chart⚓︎

Gantt Chart.

Note: does not support resources nor task dependencies; however those could be added by extending this base class.

Removed Code⚓︎

# Just snippets of Python code that may be useful in the future
dates = sorted(set(filter(None, df_raw['start'].to_list() + df_raw['end'].to_list())))
self.axis_range = {'x': [dates[0], dates[-1]]}
View Source
"""Gantt Chart.

Note: does not support resources nor task dependencies; however those could be added by extending this base class.

# Removed Code

```py
# Just snippets of Python code that may be useful in the future
dates = sorted(set(filter(None, df_raw['start'].to_list() + df_raw['end'].to_list())))
self.axis_range = {'x': [dates[0], dates[-1]]}

”“”

import plotly.graph_objects as go from palettable.tableau import TableauMedium_10

from .utils_data import format_unix, get_unix from .utils_fig import CustomChart

class GanttChart(CustomChart): # noqa: H601 “”“Gantt Chart: task and milestone timeline.”“”

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
date_format = '%Y-%m-%d'
"""Date format for bar chart."""

pallette = TableauMedium_10.hex_colors
"""Default color pallette for project colors."""

hover_label_settings = {'bgcolor': 'white', 'font_size': 12, 'namelength': 0}
"""Plotly hover label settings."""

rh = 1
"""Height of each rectangular task."""

def create_traces(self, df_raw):
    """Return traces for plotly chart.

    Args:
        df_raw: pandas dataframe with columns: `(category, label, start, end, progress)`

    Returns:
        list: Dash chart traces

    """
    # If start is None, assign end to start so that the sort is correct
    start_index = df_raw.columns.get_loc('start')
    end_index = df_raw.columns.get_loc('end')
    for index in [idx for idx, is_na in enumerate(df_raw['start'].isna()) if is_na]:
        df_raw.iloc[index, start_index] = df_raw.iloc[index, end_index]
    df_raw['progress'] = df_raw['progress'].fillna(0)  # Fill possibly missing progress values for milestones
    df_raw = (
        df_raw
        .sort_values(by=['category', 'start'], ascending=False)
        .sort_values(by=['end'], ascending=False)
        .reset_index(drop=True)
    )
    # Create color lookup using categories in sorted order
    categories = set(df_raw['category'])
    self.color_lookup = {cat: self.pallette[idx] for idx, cat in enumerate(categories)}
    # Track which categories have been plotted
    plotted_categories = []
    # Create the Gantt traces
    traces = []
    for task in df_raw.itertuples():
        y_pos = task.Index
        is_first = task.category not in plotted_categories
        plotted_categories.append(task.category)
        traces.append(self._create_task_shape(task, y_pos, is_first))
        if task.progress > 0:
            traces.append(self._create_progress_shape(task, y_pos))
        traces.append(self._create_annotation(task, y_pos))
    return traces

def _create_hover_text(self, task):
    """Return hover text for given trace.

    Args:
        task: row tuple from df_raw with: `(category, label, start, end, progress)`

    Returns:
        string: HTML-formatted hover text

    """
    dates = [format_unix(get_unix(str_ts, self.date_format), '%a, %d%b%Y') for str_ts in [task.start, task.end]]
    if task.start != task.end:
        date_range = f'<br><b>Start</b>: {dates[0]}<br><b>End</b>: {dates[1]}'
    else:
        date_range = f'<br><b>Milestone</b>: {dates[1]}'
    return f'<b>{task.category}</b><br>{task.label} ({int(task.progress * 100)}%)<br>{date_range}'

def _create_task_shape(self, task, y_pos, is_first):
    """Create colored task scatter rectangle.

    Args:
        task: row tuple from df_raw with: `(category, label, start, end, progress)`
        y_pos: top y-coordinate of task
        is_first: if True, this is the first time a task of this category will be plotted

    Returns:
        trace: single Dash chart Scatter trace

    """
    color = self.color_lookup[task.category]
    scatter_kwargs = {
        'fill': 'toself',
        'fillcolor': color,
        'hoverlabel': self.hover_label_settings,
        'legendgroup': color,
        'line': {'width': 1},
        'marker': {'color': color},
        'mode': 'lines',
        'showlegend': is_first,
        'text': self._create_hover_text(task),
        'x': [task.start, task.end, task.end, task.start, task.start],
        'y': [y_pos, y_pos, y_pos - self.rh, y_pos - self.rh, y_pos],
    }
    if is_first:
        scatter_kwargs['name'] = task.category
    return go.Scatter(**scatter_kwargs)

def _create_progress_shape(self, task, y_pos):
    """Create semi-transparent white overlay `self.shapes` to indicate task progress.

    Args:
        task: row tuple from df_raw with: `(category, label, start, end, progress)`
        y_pos: top y-coordinate of task

    Returns:
        trace: single Dash chart Scatter trace

    """
    unix_start = get_unix(task.start, self.date_format)
    unix_progress = (get_unix(task.end, self.date_format) - unix_start) * task.progress + unix_start
    end = format_unix(unix_progress, self.date_format)
    return go.Scatter(
        fill='toself',
        fillcolor='white',
        hoverinfo='skip',
        legendgroup=self.color_lookup[task.category],
        line={'width': 1},
        marker={'color': 'white'},
        mode='lines',
        opacity=0.5,
        showlegend=False,
        x=[task.start, end, end, task.start, task.start],
        y=[y_pos, y_pos, y_pos - self.rh, y_pos - self.rh, y_pos],
    )

def _create_annotation(self, task, y_pos):
    """Add task label to chart as text overlay.

    Args:
        task: row tuple from df_raw with: `(category, label, start, end, progress)`
        y_pos: top y-coordinate of task

    Returns:
        trace: single Dash chart Scatter trace

    """
    # For milestones with narrow fill, hover can be tricky, so intended to make the whole length of the text
    #   hoverable, but only the x/y point appears to be hoverable although it makes a larger hover zone at least
    return go.Scatter(
        hoverlabel=self.hover_label_settings,
        hovertemplate=self._create_hover_text(task) + '<extra></extra>',
        hovertext=self._create_hover_text(task),
        legendgroup=self.color_lookup[task.category],
        mode='text',
        showlegend=False,
        text=task.label,
        textposition='middle left',
        x=[task.end],
        y=[y_pos - self.rh / 2],
    )

def create_layout(self):
    """Extend the standard layout.

    Returns:
        dict: layout for Dash figure

    """
    layout = super().create_layout()
    # Suppress Y axis ticks/grid
    layout['yaxis']['showgrid'] = False
    layout['yaxis']['showticklabels'] = False
    layout['yaxis']['zeroline'] = False
    return layout

```

Classes⚓︎

GanttChart⚓︎

class GanttChart(
    *,
    title,
    xlabel,
    ylabel,
    layout_overrides=()
)
View Source
class GanttChart(CustomChart):  # noqa: H601
    """Gantt Chart: task and milestone timeline."""

    date_format = '%Y-%m-%d'
    """Date format for bar chart."""

    pallette = TableauMedium_10.hex_colors
    """Default color pallette for project colors."""

    hover_label_settings = {'bgcolor': 'white', 'font_size': 12, 'namelength': 0}
    """Plotly hover label settings."""

    rh = 1
    """Height of each rectangular task."""

    def create_traces(self, df_raw):
        """Return traces for plotly chart.

        Args:
            df_raw: pandas dataframe with columns: `(category, label, start, end, progress)`

        Returns:
            list: Dash chart traces

        """
        # If start is None, assign end to start so that the sort is correct
        start_index = df_raw.columns.get_loc('start')
        end_index = df_raw.columns.get_loc('end')
        for index in [idx for idx, is_na in enumerate(df_raw['start'].isna()) if is_na]:
            df_raw.iloc[index, start_index] = df_raw.iloc[index, end_index]
        df_raw['progress'] = df_raw['progress'].fillna(0)  # Fill possibly missing progress values for milestones
        df_raw = (
            df_raw
            .sort_values(by=['category', 'start'], ascending=False)
            .sort_values(by=['end'], ascending=False)
            .reset_index(drop=True)
        )
        # Create color lookup using categories in sorted order
        categories = set(df_raw['category'])
        self.color_lookup = {cat: self.pallette[idx] for idx, cat in enumerate(categories)}
        # Track which categories have been plotted
        plotted_categories = []
        # Create the Gantt traces
        traces = []
        for task in df_raw.itertuples():
            y_pos = task.Index
            is_first = task.category not in plotted_categories
            plotted_categories.append(task.category)
            traces.append(self._create_task_shape(task, y_pos, is_first))
            if task.progress > 0:
                traces.append(self._create_progress_shape(task, y_pos))
            traces.append(self._create_annotation(task, y_pos))
        return traces

    def _create_hover_text(self, task):
        """Return hover text for given trace.

        Args:
            task: row tuple from df_raw with: `(category, label, start, end, progress)`

        Returns:
            string: HTML-formatted hover text

        """
        dates = [format_unix(get_unix(str_ts, self.date_format), '%a, %d%b%Y') for str_ts in [task.start, task.end]]
        if task.start != task.end:
            date_range = f'<br><b>Start</b>: {dates[0]}<br><b>End</b>: {dates[1]}'
        else:
            date_range = f'<br><b>Milestone</b>: {dates[1]}'
        return f'<b>{task.category}</b><br>{task.label} ({int(task.progress * 100)}%)<br>{date_range}'

    def _create_task_shape(self, task, y_pos, is_first):
        """Create colored task scatter rectangle.

        Args:
            task: row tuple from df_raw with: `(category, label, start, end, progress)`
            y_pos: top y-coordinate of task
            is_first: if True, this is the first time a task of this category will be plotted

        Returns:
            trace: single Dash chart Scatter trace

        """
        color = self.color_lookup[task.category]
        scatter_kwargs = {
            'fill': 'toself',
            'fillcolor': color,
            'hoverlabel': self.hover_label_settings,
            'legendgroup': color,
            'line': {'width': 1},
            'marker': {'color': color},
            'mode': 'lines',
            'showlegend': is_first,
            'text': self._create_hover_text(task),
            'x': [task.start, task.end, task.end, task.start, task.start],
            'y': [y_pos, y_pos, y_pos - self.rh, y_pos - self.rh, y_pos],
        }
        if is_first:
            scatter_kwargs['name'] = task.category
        return go.Scatter(**scatter_kwargs)

    def _create_progress_shape(self, task, y_pos):
        """Create semi-transparent white overlay `self.shapes` to indicate task progress.

        Args:
            task: row tuple from df_raw with: `(category, label, start, end, progress)`
            y_pos: top y-coordinate of task

        Returns:
            trace: single Dash chart Scatter trace

        """
        unix_start = get_unix(task.start, self.date_format)
        unix_progress = (get_unix(task.end, self.date_format) - unix_start) * task.progress + unix_start
        end = format_unix(unix_progress, self.date_format)
        return go.Scatter(
            fill='toself',
            fillcolor='white',
            hoverinfo='skip',
            legendgroup=self.color_lookup[task.category],
            line={'width': 1},
            marker={'color': 'white'},
            mode='lines',
            opacity=0.5,
            showlegend=False,
            x=[task.start, end, end, task.start, task.start],
            y=[y_pos, y_pos, y_pos - self.rh, y_pos - self.rh, y_pos],
        )

    def _create_annotation(self, task, y_pos):
        """Add task label to chart as text overlay.

        Args:
            task: row tuple from df_raw with: `(category, label, start, end, progress)`
            y_pos: top y-coordinate of task

        Returns:
            trace: single Dash chart Scatter trace

        """
        # For milestones with narrow fill, hover can be tricky, so intended to make the whole length of the text
        #   hoverable, but only the x/y point appears to be hoverable although it makes a larger hover zone at least
        return go.Scatter(
            hoverlabel=self.hover_label_settings,
            hovertemplate=self._create_hover_text(task) + '<extra></extra>',
            hovertext=self._create_hover_text(task),
            legendgroup=self.color_lookup[task.category],
            mode='text',
            showlegend=False,
            text=task.label,
            textposition='middle left',
            x=[task.end],
            y=[y_pos - self.rh / 2],
        )

    def create_layout(self):
        """Extend the standard layout.

        Returns:
            dict: layout for Dash figure

        """
        layout = super().create_layout()
        # Suppress Y axis ticks/grid
        layout['yaxis']['showgrid'] = False
        layout['yaxis']['showticklabels'] = False
        layout['yaxis']['zeroline'] = False
        return layout

Ancestors (in MRO)⚓︎

  • dash_charts.utils_fig.CustomChart

Class variables⚓︎

annotations
date_format

Date format for bar chart.

hover_label_settings

Plotly hover label settings.

pallette

Default color pallette for project colors.

rh

Height of each rectangular task.

Instance variables⚓︎

axis_range

Specify x/y axis range or leave as empty dictionary for autorange.

Methods⚓︎

apply_custom_layout⚓︎

def apply_custom_layout(
    self,
    layout
)

Extend and/or override layout with custom settings.

Parameters:

Name Description
layout base layout dictionary. Typically from self.create_layout()

Returns:

Type Description
dict layout for Dash figure
View Source
    def apply_custom_layout(self, layout):
        """Extend and/or override layout with custom settings.

        Args:
            layout: base layout dictionary. Typically from self.create_layout()

        Returns:
            dict: layout for Dash figure

        """
        for parent_key, sub_key, value in self.layout_overrides:
            if sub_key is not None:
                layout[parent_key][sub_key] = value
            else:
                layout[parent_key] = value

        return layout

create_figure⚓︎

def create_figure(
    self,
    df_raw,
    **kwargs_data
)

Create the figure dictionary.

Parameters:

Name Description
df_raw data to pass to formatter method
kwargs_data keyword arguments to pass to the data formatter method

Returns:

Type Description
dict keys data and layout for Dash
View Source
    def create_figure(self, df_raw, **kwargs_data):
        """Create the figure dictionary.

        Args:
            df_raw: data to pass to formatter method
            kwargs_data: keyword arguments to pass to the data formatter method

        Returns:
            dict: keys `data` and `layout` for Dash

        """
        return {
            'data': self.create_traces(df_raw, **kwargs_data),
            'layout': go.Layout(self.apply_custom_layout(self.create_layout())),
        }

create_layout⚓︎

def create_layout(
    self
)

Extend the standard layout.

Returns:

Type Description
dict layout for Dash figure
View Source
    def create_layout(self):
        """Extend the standard layout.

        Returns:
            dict: layout for Dash figure

        """
        layout = super().create_layout()
        # Suppress Y axis ticks/grid
        layout['yaxis']['showgrid'] = False
        layout['yaxis']['showticklabels'] = False
        layout['yaxis']['zeroline'] = False
        return layout

create_traces⚓︎

def create_traces(
    self,
    df_raw
)

Return traces for plotly chart.

Parameters:

Name Description
df_raw pandas dataframe with columns: (category, label, start, end, progress)

Returns:

Type Description
list Dash chart traces
View Source
    def create_traces(self, df_raw):
        """Return traces for plotly chart.

        Args:
            df_raw: pandas dataframe with columns: `(category, label, start, end, progress)`

        Returns:
            list: Dash chart traces

        """
        # If start is None, assign end to start so that the sort is correct
        start_index = df_raw.columns.get_loc('start')
        end_index = df_raw.columns.get_loc('end')
        for index in [idx for idx, is_na in enumerate(df_raw['start'].isna()) if is_na]:
            df_raw.iloc[index, start_index] = df_raw.iloc[index, end_index]
        df_raw['progress'] = df_raw['progress'].fillna(0)  # Fill possibly missing progress values for milestones
        df_raw = (
            df_raw
            .sort_values(by=['category', 'start'], ascending=False)
            .sort_values(by=['end'], ascending=False)
            .reset_index(drop=True)
        )
        # Create color lookup using categories in sorted order
        categories = set(df_raw['category'])
        self.color_lookup = {cat: self.pallette[idx] for idx, cat in enumerate(categories)}
        # Track which categories have been plotted
        plotted_categories = []
        # Create the Gantt traces
        traces = []
        for task in df_raw.itertuples():
            y_pos = task.Index
            is_first = task.category not in plotted_categories
            plotted_categories.append(task.category)
            traces.append(self._create_task_shape(task, y_pos, is_first))
            if task.progress > 0:
                traces.append(self._create_progress_shape(task, y_pos))
            traces.append(self._create_annotation(task, y_pos))
        return traces

initialize_mutables⚓︎

def initialize_mutables(
    self
)

Initialize the mutable data members to prevent modifying one attribute and impacting all instances.

View Source
    def initialize_mutables(self):
        """Initialize the mutable data members to prevent modifying one attribute and impacting all instances."""
        ...

Last update: August 5, 2022
Created: August 5, 2022